使用Inherited Widget 来共享地理位置信息

英文地址: Sharing Location using Inherited Widget

某些时候,你需要在你的应用程序中多个路由或者控件中使用一个位置信息,在所有需要的地方配置和管理flutter的location插件。相反的是,我们可使用Inherited Widget控件来在多个控件之间共享数据。

从命令行敲入如下代码flutter create -t package location_context -t参数是选择模版,-t package意思是快速创建一个模块包的模版样本代码。

第一步:导入依赖

pubspec.yaml文件的dependences:

1
2
location: ^1.3.4
quiver: ^2.0.1

这个location插件用于与GPS硬件进行交互,来获取设备真实的位置信息用于共享数据。

这个quiver提供的hashObjects便捷方法,将用于创建Position类的hashCode。备注:flutter内部也使用这个包,所以我们应该确保版本同步,如果依赖发生冲突,将会产生一个警告。

当我们在pubspec.yaml文件中添加这些依赖后,运行flutter packages get来下载安装获取这些依赖包。

第二步: 创建代码库

打开lib/location_context.dart,这个是我们包的入口,除了library第一行行外,替换掉生成默认的样本代码。

1
2
3
4
5
6
7
8
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/serivces.dart';
import 'package:location/location.dart';
import 'package:quiver/core.dart';

part 'src/postion.dart';
part 'src/context.dart';

该文件是我们包的入口,并且使用part来导入我们其他的文件,我们所有的导入都放到这个文件里面,即使我们不在这个文件中使用,这将会part的文件中使用。

使用part可以允许我们将一个库分割成多个dart文件。因为它们全部都是是该库的一部分(同一个库),私有的变量也可以在所有指定的文件中访问到,提到这里是因为Position有个私有的构造函数,希望在LocationContext中也能使用到。

第三步: 创建Position类

创建lib/src/position.dart,用来定义Position类,用于装载携带我们的位置数据。location插件将会返回返回一个Map对象。之后我们将会使用这个对象来初始化我们的Position实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
part of location_context; // 与part 是成对出现的,表示在该库内

class Position {
final double latitude; // 纬度
final double longitude; // 经度
final double accuracy; // 精度
final double altitude; //海拔高度
final double speed; //速度
final double speedAccuracy; //速度精度

final _hashCode; // 私有成员变量
// 构造参数
Position({this.latitude,this.longitude,this.accuracy,
this.altitude,this.speed,this.speedAccuracy}):
_hashCode = hashObjects([latitude,longitude,accuracy,
altitude,speed,speedAccuracy])
// 命名构造函数,私有,保证只有本库能够访问到
Position._fromMap(Map<String, double> data)
: this(
latitude: data['latitude'],
longitude: data['longitude'],
accuracy: data['accuracy'],
altitude: data['altitude'],
speed: data['speed'],
speedAccuracy: data['speed_accuracy'],
);
}

所有的这些属性都是location插件会返回的数据项,我们也定义了一个构造函数来让我们能够手动地定义每个属性。

因为所有的属性变量是final,运行时不可变,我也将会使用Quiver的hashObjects方法来计算我们的_hashCode,这样不需要每次需要的时候来计算它,因为它不会变。

此外,我们还需要能够与其他对象进行比较,以及能够优雅地以字符串的形式输出这些对象用于调试等等,在_fromMap构造函数下添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
bool operator ==(dynamic other) {
if (other is! Position) return false;
return hashCode == other.hashCode;
}
@override
int get hashCode => _hashCode; //getter属性

@overrive
String toString() {
return 'Position($latitude,$longitude,$accuracy,$speed,$speedAccuracy)';
}

首先我们重写了我们的==运算符,来做一个比较的运算,通过比较thisotherhashCode来判断它们包含的值是否一致,如果hashCode的值一致,那么它们将包含相同的数据。

创建Location Context

创建lib/src/context.dart,该文件将会包含Inherited Widget来实现我们的魔法。当我们创建一个控件的时候,我需要考虑一下几点:

  1. 为了单元测试,我们需要能够从该包中mock这个Location对象。
  2. 我们需要实际创建这个继承的控件,来共享我们的存储的数据。
  3. 我们能够更新以及存储这些共享的数据值。

我们在第二步中,在

Location 依赖注入

1
2
3
4
5
6
7
8
9
10
11
part of location_context;

@visibleForTesting
typedef Location LocationFactory(); // typedef 类型定义

@visibleForTesting
void mockLocation(LocationFactory mock) {
_createLocation = mock;
}

LocationFactory _createLocation = () => Location();

首先,我们使用typedef为我们的工厂函数进行了类型定义,我将用该工厂函数来进行依赖注入,方法代码仅仅是不接受任务参数来返回Location实例。

其次,我定义了一个方法能够覆盖我们的工厂方法,以至于能够注入不同的依赖。mockLocation接收一个回调函数作为依赖,然后将该回调赋值给默认的工厂函数。

最后,定义我们的默认的工厂函数来返回我们Location依赖的实例对象。在测试的时候我们可以使用mockLocation方法来允许我们的控件能够获取到一个mocked的版本。

我们使用@visibleForTesting标记这两部分进行模拟,来让开发者知道这些构造是“私有”,在他们的代码中不应该访问的,即使在这个包外是可见的。

这个Inherited Widget继承控件本质非常简单,我们传给它一些信息,然后这些信息会在控件树层次中高效地向下传播,它控件下的子控件能够访问到这些信息,达到信息共享的效果,我们将共享三个值:lastLocationcurrentLocation,和error

创建Inerited Widget

lastLocationcurrentLocation这个两个变量不言自明,使我们的应用程序能够进行方向向量上的计算。error仅仅是一个字符串字段,来包含我们可能遇到的异常错误信息,该控件的父类是ProxyWidget代理控件,该控件不会去构建一个新的控件,而是使用提供给它的子控件,也就是child属性。

下面仅仅是这个类的开始部分,我们将在随后的部分进行修改调整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class LocationContext extends InheritedWidget { // 继承InheritedWidget
final Position currentLocation;
final Position lastLocation;
final String error; // 定义我们要共享的成员变量,为初始化默认null;

LocationConext._({ // 私有的命名构造函数,继承额外添加的成员变量在我们的构造参数中,初始化
@required this.currentLocation,
this.lastLocation,
this.error,
Key key,
Widget child,
}): super(key :key,child: child); // 调用父类构造函数

static Widget around(Widget child,{Key key}) {
return _LocationContextWrapper(child:child,key:key)
}

@override
bool updateShouldNotify(LocationContext oldWidget) {
return currentLocation != oldWidget.currentLocation ||
lastLocation != oldWidget.lastLocation ||
error != oldWidget.error;
}
}

我们的命名构造函数以_为前缀,这意味着它只能在本身所在的包中进行初始化,这对继承控件来说十分重要的。

of方法正是发挥魔力的地方,该方法允许任意的子控件能够调用LocationConetext.of(context),然后返回该控件的实例,允许它们能够访问到该控件的公共成员。

最后,updateShouldNotify用于是否通知控件树层次结构中该控件节点以下的控件已经发生更新,以至于它们能够重新构建,如果必要 ,如果其属性发生变化,这个控件将会通知一个更新。

创建Stateful Widget

我们的状态控件将会与Location进行交互,存储并更新我们的位置信息,并将其传入我们创建的继承控件中。

添加下面代码到lib/src/conetext.dart底部:

1
2
3
4
5
6
7
8
9
10
11
12
13
class _LocationContextWrapper extends StatefulWidget {
final Widget child;
_locationContextWrapper({Key key,this.child}) : super(key:key);
State<StatefulWidget> createState() => _LocationContextWrapperState();
}

class _LocationContextWrapperState extends State(_LocationContextWrapper) {
final Location _location = _createLocation(); // 注入我们的依赖,初始化Location对象
String _error;
Position _currentLocation;
Position _lastLocation;
StreamSubscription<Map<String,double>> _locationChangedSubsciption;
}

我们有一个StatefulWidget控件且包含了一个名为child的成员控件,这个child控件将会被LocationContext`包裹,接收位置更新。

你将会注意到我们的三个成员变量和我们LocationContext存储的三个变量很相似,这个因为这三个_error,_currentLocation,_lastLocation值正是我们将要本地管理并传入LocationContext的值。

最后,_locationChangedSubsription将用于从我们的Location对象来监听位置更新,注意这个将会返回一个Map对象,需要将其转化为Map实例。

接下来初始化我们的状态,在_locationChangedSubscription下添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
@override
void initState(){
super.initState();
_locationChangedSubscription = _location.onLocationChanged().listen((Map<String,double> result) {
final Position nextLocation = Position._fromMap(result);
setState((){
_error = null;
_lastLocation = _currentLocation;
_currentLocation = nextLocation;
});
});
initLocation();
}

我们在这个方法中,做了两个事情:1.我们订阅_location.onLocationChanged流来监听位置更新,每当接收到一个更新,我们将会从Map中创建一个Position实例,然后从接收的位置信息中通过清空_error,更新_lastLocation,设置_currentLocation来更新我们的state,2.初始化位置信息,来载入初始的位置数据,在这个方法我们使用async/await语法糖来使得我们的代码看起来更加线性,但是我们也可以仅仅使用Future,添加如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void initLocation() async {
try {
final Map<String, double> result = await _location.getLocation();

setState(() {
_error = null;
_lastLocation = Position._fromMap(result);
_currentLocation = _lastLocation;
});
} on PlatformException catch (e) {
setState(() {
if (e.code == 'PERMISSION_DENIED') {
_error = 'Location Permission Denied';
} else if (e.code == 'PERMISSION_DENIED_NEVER_ASK') {
_error =
'Location Permission Denied. Please open App Settings and enabled Location Permissions';
}
});
}
}

我们await当前返回设备位置信息,然后像我们之前的那样更新我们的状态,因为这个是我们第一次时候的状态,我将要设置_lastLocation_currentLoaction相同的值,如果发生异常,我们捕获该异常,设置_error

接下来,我们要确保正确地取消订阅流,这将会在dispose中处理,该方法会在控件销毁的时候被调用。

1
2
3
4
5
@override
void dispose() {
_locationChangedSubscription?.cancel();
super.dispose();
}

这里使用?.确保null值安全,这个要小心,如果没有初始化这个订阅,这个可能会发生错误。所以做一个空值判断。

最后,我们将会利用这些信息来实际构建,下面这个地方是我们实际使用Inherited Widget这个代理控件的地方:

1
2
3
4
5
6
7
8
9
@override
Widget bulid(BuildContext context) {
return LocationContext._(
lastLocation:_lastLocation,
currentLocation: _currentLocation,
error:_error,
child:widget.child // widget == _LocationContextWrapper,其为getter属性
)
}

简单地说,我们只是构建了Inherited Widget,然后将在State中收集到的数据传给它,同时Inherited Widget包裹childInherited Widget控件为代理控件,本身不会构建子控件,相反会将child成员控件作为其子控件。这就意味着无论何时我们获取一个新的位置以及更新我们的状态,就会传入新的信息给LocationContext,进而子节点的控件能够访问到这些数据。

此外,我们缺少了些重要的东西,一个使用_LocationContextWrapper的方法,因为它是包内私有的,我们需要添加一个方法来帮助我们使用它,我添加一个公有的静态方法到LocationContext中。

1
2
3
static Widget around(Widget child, {Key key}) {
retutn _locationContextWrapper(child:child,key:key);
}